// -------------------------------------------------
// BlinKit - BlinKit Library
// -------------------------------------------------
//   File Name: theme_helpers.cpp
// Description: Theme Helper Classes
//      Author: Ziming Li
//     Created: 2022-04-26
// -------------------------------------------------
// Copyright (C) 2022 MingYang Software Technology.
// -------------------------------------------------

#include "./theme_helpers.h"

#include <vsstyle.h>
#include "blinkit/blink/renderer/platform/geometry/int_rect.h"
#include "third_party/zed/include/zed/container_utilites.hpp"
#include "third_party/zed/include/zed/memory.hpp"
#include "third_party/zed/include/zed/file/path.hpp"

namespace BlinKit {

ThemeDataCache::Storage::~Storage(void)
{
    for (const auto &[_, hTheme] : m_themes)
        CloseThemeData(hTheme);
}

HTHEME ThemeDataCache::Storage::Get(PCWSTR classList) const
{
    auto it = m_themes.find(classList);
    return m_themes.end() != it ? it->second : nullptr;
}

void ThemeDataCache::Storage::Save(PCWSTR classList, HTHEME hTheme)
{
    ASSERT(!zed::key_exists(m_themes, classList));
    m_themes.emplace(classList, hTheme);
}

std::unique_ptr<ThemeDataCache> ThemeDataCache::Create(HMODULE hUxTheme)
{
    Creator creator = nullptr;
    if (zed::hmodule::get_proc_address(hUxTheme, "OpenThemeDataForDpi", creator))
        return std::make_unique<ThemeDataCacheEx>(creator);
    return zed::wrap_unique(new ThemeDataCache(OpenThemeWrapper));
}

ThemeDataCache::Storage& ThemeDataCache::GetStorage(UINT dpi)
{
    ASSERT(96 == dpi);
    return m_storage;
}

HTHEME WINAPI ThemeDataCache::OpenThemeWrapper(HWND h, PCWSTR classList, UINT)
{
    return OpenThemeData(h, classList);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

ThemeDataCache::Storage& ThemeDataCacheEx::GetStorage(UINT dpi)
{
    static_assert(6 == DPI_LEVELS, "TODO: Process more DPI level(s)!");
    switch (dpi)
    {
        case 120:
            return m_otherStorages[0];
        case 144:
            return m_otherStorages[1];
        case 192:
            return m_otherStorages[2];
        case 216:
            return m_otherStorages[3];
        case 240:
            return m_otherStorages[4];
    }
    return ThemeDataCache::GetStorage(dpi);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

std::unique_ptr<ThemePainter> ThemePainter::Create(void)
{
    std::wstring dll = zed::sys_path::get_system_path();
    dll.append(L"UxTheme.dll");

    HMODULE hUxTheme = LoadLibraryW(dll.c_str());
    if (nullptr != hUxTheme)
    {
        if (IsThemeActive())
            return std::make_unique<UxThemePainter>(hUxTheme);

        FreeLibrary(hUxTheme);
    }
    return std::make_unique<User32ThemePainter>();
}

void ThemePainter::FillSolidRect(HDC hdc, const RECT &rc, COLORREF color)
{
    COLORREF oldColor = SetBkColor(hdc, color);
    ExtTextOut(hdc, 0, 0, ETO_OPAQUE, &rc, nullptr, 0, nullptr);
    SetBkColor(hdc, oldColor);
}

void ThemePainter::Paint(HDC hdc, Part part, State state, const IntRect &rect, const ExtraParams *extra)
{
    switch (part)
    {
        case Part::PartScrollbarCorner:
        {
            RECT rc = { 0 };
            rc.right = rect.width();
            rc.bottom = rect.height();
            FillSolidRect(hdc, rc, GetSysColor(COLOR_WINDOW));
            break;
        }
        default:
            ASSERT_NOT_REACHED();
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

void User32ThemePainter::Paint(HDC hdc, Part part, State state, const IntRect &rect, const ExtraParams *extra)
{
    ASSERT(false); // BKTODO:
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

struct UxThemePainter::ComboBoxState {
    int state = CBXS_NORMAL;
    int borderState = CBB_NORMAL;
};

UxThemePainter::Session::Session(HDC hdc, ThemeDataCache &cache) : Session(GetDeviceCaps(hdc, LOGPIXELSY), cache)
{
    ASSERT(GetDeviceCaps(hdc, LOGPIXELSX) == m_dpi);
}

UxThemePainter::Session::Session(UINT dpi, ThemeDataCache &cache)
    : m_dpi(dpi), m_creator(cache.GetCreator()), m_storage(cache.GetStorage(dpi))
{
}

void UxThemePainter::Session::Draw(HDC hdc, PCWSTR classList, int partId, int stateId, const RECT &rect, const DTBGOPTS *options)
{
    HTHEME hTheme = m_storage.Get(classList);
    if (nullptr == hTheme)
    {
        hTheme = m_creator(nullptr, classList, m_dpi);
        ASSERT(nullptr != hTheme);
        m_storage.Save(classList, hTheme);
    }
    DrawThemeBackgroundEx(hTheme, hdc, partId, stateId, &rect, options);
}

void UxThemePainter::Session::Draw(HDC hdc, PCWSTR classList, int partId, int stateId, const IntSize &size)
{
    RECT rc = { 0 };
    rc.right = size.width();
    rc.bottom = size.height();
    return Draw(hdc, classList, partId, stateId, rc);
}

UxThemePainter::UxThemePainter(HMODULE hUxTheme) : m_hUxTheme(hUxTheme), m_cache(ThemeDataCache::Create(hUxTheme))
{
}

UxThemePainter::~UxThemePainter(void)
{
    FreeLibrary(m_hUxTheme);
}

int UxThemePainter::AdjustButtonStateId(State state, const ButtonExtraParams &extra)
{
    switch (state)
    {
        case State::StateHover:
            return PBS_HOT;
        case State::StatePressed:
            return PBS_PRESSED;
        case State::StateDisabled:
            return PBS_DISABLED;
        default:
            ASSERT(State::StateNormal == state);
    }
    return extra.isDefault ? PBS_DEFAULTED : PBS_NORMAL;
}

int UxThemePainter::AdjustCheckboxStateId(State state, const ButtonExtraParams &extra)
{
    if (extra.checked)
    {
        switch (state)
        {
            case State::StateDisabled:
                return CBS_CHECKEDDISABLED;
            case State::StateHover:
                return CBS_CHECKEDHOT;
            case State::StatePressed:
                return CBS_CHECKEDPRESSED;
            default:
                ASSERT(State::StateNormal == state);
        }
        return CBS_CHECKEDNORMAL;
    }

    if (extra.indeterminate)
    {
        switch (state)
        {
            case State::StateDisabled:
                return CBS_MIXEDDISABLED;
            case State::StateHover:
                return CBS_MIXEDHOT;
            case State::StatePressed:
                return CBS_MIXEDPRESSED;
            default:
                ASSERT(State::StateNormal == state);
        }
        return CBS_MIXEDNORMAL;
    }

    switch (state)
    {
        case State::StateDisabled:
            return CBS_UNCHECKEDDISABLED;
        case State::StateHover:
            return CBS_UNCHECKEDHOT;
        case State::StatePressed:
            return CBS_UNCHECKEDPRESSED;
        default:
            ASSERT(State::StateNormal == state);
    }
    return CBS_UNCHECKEDNORMAL;
}

UxThemePainter::ComboBoxState UxThemePainter::AdjustComboBoxState(State state)
{
    ComboBoxState ret;
    switch (state)
    {
        case State::StateDisabled:
            ret.state = CBXS_DISABLED;
            ret.borderState = CBB_DISABLED;
            break;
        case State::StateHover:
            ret.state = CBXS_HOT;
            ret.borderState = CBB_HOT;
            break;
        case State::StatePressed:
            ret.state = CBXS_PRESSED;
            ret.borderState = CBB_FOCUSED;
            break;
        default:
            ASSERT(State::StateNormal == state);
    }
    return ret;
}

int UxThemePainter::AdjustRadioStateId(State state, const ButtonExtraParams &extra)
{
    if (extra.checked)
    {
        switch (state)
        {
            case State::StateDisabled:
                return RBS_CHECKEDDISABLED;
            case State::StateHover:
                return RBS_CHECKEDHOT;
            case State::StatePressed:
                return RBS_CHECKEDPRESSED;
            default:
                ASSERT(State::StateNormal == state);
        }
        return RBS_CHECKEDNORMAL;
    }

    switch (state)
    {
        case State::StateDisabled:
            return RBS_UNCHECKEDDISABLED;
        case State::StateHover:
            return RBS_UNCHECKEDHOT;
        case State::StatePressed:
            return RBS_UNCHECKEDPRESSED;
        default:
            ASSERT(State::StateNormal == state);
    }
    return RBS_UNCHECKEDNORMAL;
}

void UxThemePainter::Paint(HDC hdc, Part part, State state, const IntRect &rect, const ExtraParams *extra)
{
    Session session(hdc, *m_cache);
    switch (part)
    {
        case Part::PartScrollbarDownArrow:
        case Part::PartScrollbarLeftArrow:
        case Part::PartScrollbarRightArrow:
        case Part::PartScrollbarUpArrow:
            PaintScrollArrow(session, hdc, part, state, rect.size(), extra->scrollbarTrack);
            break;
        case Part::PartScrollbarHorizontalThumb:
        case Part::PartScrollbarVerticalThumb:
        case Part::PartScrollbarHorizontalTrack:
        case Part::PartScrollbarVerticalTrack:
            PaintScrollPart(session, hdc, part, state, rect.size(), extra->scrollbarTrack);
            break;
        case Part::PartCheckbox:
        case Part::PartRadio:
        case Part::PartButton:
            PaintButton(session, hdc, part, state, rect.size(), extra->button);
            break;
        case Part::PartTextField:
            PaintTextField(session, hdc, state, rect.size(), extra->textField);
            break;
        case Part::PartMenuList:
            PaintMenuList(session, hdc, state, rect, extra->menuList);
            break;
        case Part::PartProgressBar:
            PaintProgressBar(session, hdc, state, rect, extra->progressBar);
            break;
        default:
            ThemePainter::Paint(hdc, part, state, rect, extra);
    }
}

void UxThemePainter::PaintButton(
    Session &session,
    HDC hdc,
    Part part, State state, const IntSize &size,
    const ButtonExtraParams &extra)
{
    int partId = -1, stateId = -1;
    switch (part)
    {
        case Part::PartCheckbox:
            partId = BP_CHECKBOX;
            stateId = AdjustCheckboxStateId(state, extra);
            break;
        case Part::PartRadio:
            partId = BP_RADIOBUTTON;
            stateId = AdjustRadioStateId(state, extra);
            break;
        default:
            ASSERT(Part::PartButton == part);
            partId = BP_PUSHBUTTON;
            stateId = AdjustButtonStateId(state, extra);
    }

    session.Draw(hdc, VSCLASS_BUTTON, partId, stateId, size);
}

void UxThemePainter::PaintMenuList(
    Session &session,
    HDC hdc,
    State state, const IntRect &rect,
    const MenuListExtraParams &extra)
{
    ComboBoxState comboBoxState = AdjustComboBoxState(state);

    session.Draw(hdc, VSCLASS_COMBOBOX, CP_BACKGROUND, comboBoxState.state, rect.size());
    session.Draw(hdc, VSCLASS_COMBOBOX, CP_BORDER, comboBoxState.borderState, rect.size());

    RECT rcButton;
    rcButton.left = rect.width() - rect.height();
    rcButton.top = 0;
    rcButton.right = rcButton.left + rect.height();
    rcButton.bottom = rect.height();
    session.Draw(hdc, VSCLASS_COMBOBOX, CP_DROPDOWNBUTTON, comboBoxState.state, rcButton);
}

void UxThemePainter::PaintProgressBar(
    Session &session,
    HDC hdc,
    State state, const IntRect &rect,
    const ProgressBarExtraParams &extra)
{
    session.Draw(hdc, VSCLASS_PROGRESS, PP_BAR, 0, rect.size());

    RECT rcValue;
    rcValue.left = extra.valueRectX - rect.x();
    rcValue.top = extra.valueRectY - rect.y();
    rcValue.right = extra.valueRectWidth;
    rcValue.bottom = extra.valueRectHeight;

    if (!extra.determinate)
    {
        ASSERT(false); // BKTODO:
    }

    DTBGOPTS options = { 0 };
    options.dwSize = sizeof(DTBGOPTS);
    if (rect.width() == rcValue.right && rect.x() != rcValue.left)
        options.dwFlags = DTBG_MIRRORDC;
    options.rcClip.right = rect.width();
    options.rcClip.bottom = rect.height();
    session.Draw(hdc, VSCLASS_PROGRESS, PP_FILL, 0, rcValue, &options);

    // BKTODO: Draw glossy overlay?
}

void UxThemePainter::PaintScrollArrow(
    Session &session,
    HDC hdc,
    Part part, State state, const IntSize &size,
    const ScrollbarTrackExtraParams &extra)
{
    int stateId = -1;

    switch (MAKEWORD(part, state))
    {
        case MAKEWORD(Part::PartScrollbarDownArrow, State::StateDisabled):
            stateId = ABS_DOWNDISABLED;
            break;
        case MAKEWORD(Part::PartScrollbarDownArrow, State::StateHover):
            stateId = ABS_DOWNHOT;
            break;
        case MAKEWORD(Part::PartScrollbarDownArrow, State::StateNormal):
            stateId = ABS_DOWNNORMAL;
            break;
        case MAKEWORD(Part::PartScrollbarDownArrow, State::StatePressed):
            stateId = ABS_DOWNPRESSED;
            break;
        case MAKEWORD(Part::PartScrollbarLeftArrow, State::StateDisabled):
            stateId = ABS_LEFTDISABLED;
            break;
        case MAKEWORD(Part::PartScrollbarLeftArrow, State::StateHover):
            stateId = ABS_LEFTHOT;
            break;
        case MAKEWORD(Part::PartScrollbarLeftArrow, State::StateNormal):
            stateId = ABS_LEFTNORMAL;
            break;
        case MAKEWORD(Part::PartScrollbarLeftArrow, State::StatePressed):
            stateId = ABS_LEFTPRESSED;
            break;
        case MAKEWORD(Part::PartScrollbarRightArrow, State::StateDisabled):
            stateId = ABS_RIGHTDISABLED;
            break;
        case MAKEWORD(Part::PartScrollbarRightArrow, State::StateHover):
            stateId = ABS_RIGHTHOT;
            break;
        case MAKEWORD(Part::PartScrollbarRightArrow, State::StateNormal):
            stateId = ABS_RIGHTNORMAL;
            break;
        case MAKEWORD(Part::PartScrollbarRightArrow, State::StatePressed):
            stateId = ABS_RIGHTPRESSED;
            break;
        case MAKEWORD(Part::PartScrollbarUpArrow, State::StateDisabled):
            stateId = ABS_UPDISABLED;
            break;
        case MAKEWORD(Part::PartScrollbarUpArrow, State::StateHover):
            stateId = ABS_UPHOT;
            break;
        case MAKEWORD(Part::PartScrollbarUpArrow, State::StateNormal):
            stateId = ABS_UPNORMAL;
            break;
        case MAKEWORD(Part::PartScrollbarUpArrow, State::StatePressed):
            stateId = ABS_UPPRESSED;
            break;
        default:
            ASSERT_NOT_REACHED();
            return;
    }

    session.Draw(hdc, VSCLASS_SCROLLBAR, SBP_ARROWBTN, stateId, size);
}

void UxThemePainter::PaintScrollPart(
    Session &session,
    HDC hdc,
    Part part, State state, const IntSize &size,
    const ScrollbarTrackExtraParams &extra)
{
    int partId = -1, stateId = -1;
    switch (part)
    {
        case Part::PartScrollbarHorizontalThumb:
            partId = SBP_THUMBBTNHORZ;
            break;
        case Part::PartScrollbarVerticalThumb:
            partId = SBP_THUMBBTNVERT;
            break;
        case Part::PartScrollbarHorizontalTrack:
            partId = extra.isBack ? SBP_UPPERTRACKHORZ : SBP_LOWERTRACKHORZ;
            break;
        case Part::PartScrollbarVerticalTrack:
            partId = extra.isBack ? SBP_UPPERTRACKVERT : SBP_LOWERTRACKVERT;
            break;
        default:
            ASSERT_NOT_REACHED();
            return;
    }

    switch (state)
    {
        case State::StateDisabled:
            stateId = SCRBS_DISABLED;
            break;
        case State::StateHover:
            stateId = SCRBS_HOT;
            break;
        case State::StateNormal:
            stateId = SCRBS_NORMAL;
            break;
        case State::StatePressed:
            stateId = SCRBS_PRESSED;
            break;
        default:
            ASSERT_NOT_REACHED();
            return;
    }

    session.Draw(hdc, VSCLASS_SCROLLBAR, partId, stateId, size);
}

void UxThemePainter::PaintTextField(
    Session &session,
    HDC hdc,
    State state, const IntSize &size,
    const TextFieldExtraParams &extra)
{
    int stateId = -1;
    switch (state)
    {
        case State::StateNormal:
            stateId = ETS_NORMAL;
            break;
        case State::StateFocused:
            stateId = ETS_FOCUSED;
            break;
        case State::StateHover:
            stateId = ETS_HOT;
            break;
        case State::StatePressed:
            stateId = ETS_SELECTED;
            break;
        case State::StateReadonly:
            stateId = ETS_READONLY;
            break;
        case State::StateDisabled:
            stateId = ETS_DISABLED;
            break;
        default:
            ASSERT_NOT_REACHED();
            return;
    }

    session.Draw(hdc, VSCLASS_EDIT, EP_BACKGROUNDWITHBORDER, stateId, size);
}

} // namespace BlinKit
